/*
 * Copyright 2017-2020 original authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.micronaut.annotation.processing.test

import com.sun.source.util.JavacTask
import groovy.transform.CompileStatic
import io.micronaut.annotation.processing.AggregatingTypeElementVisitorProcessor
import io.micronaut.annotation.processing.JavaAnnotationMetadataBuilder
import io.micronaut.annotation.processing.JavaNativeElementsHelper
import io.micronaut.annotation.processing.ModelUtils
import io.micronaut.annotation.processing.TypeElementVisitorProcessor
import io.micronaut.annotation.processing.visitor.JavaElementFactory
import io.micronaut.annotation.processing.visitor.JavaVisitorContext
import io.micronaut.aop.internal.InterceptorRegistryBean
import io.micronaut.context.ApplicationContext
import io.micronaut.context.ApplicationContextBuilder
import io.micronaut.context.DefaultBeanDefinitionsProvider
import io.micronaut.context.Qualifier
import io.micronaut.context.event.ApplicationEventPublisherFactory
import io.micronaut.core.annotation.AnnotationMetadata
import io.micronaut.core.annotation.AnnotationMetadataProvider
import io.micronaut.core.annotation.Experimental
import org.jspecify.annotations.NonNull
import org.jspecify.annotations.Nullable
import io.micronaut.core.beans.BeanIntrospection
import io.micronaut.core.convert.value.MutableConvertibleValuesMap
import io.micronaut.core.graal.GraalReflectionConfigurer
import io.micronaut.core.io.IOUtils
import io.micronaut.core.naming.NameUtils
import io.micronaut.inject.BeanConfiguration
import io.micronaut.inject.BeanDefinition
import io.micronaut.inject.BeanDefinitionReference
import io.micronaut.inject.annotation.AbstractAnnotationMetadataBuilder
import io.micronaut.inject.annotation.AnnotationMapper
import io.micronaut.inject.annotation.AnnotationMetadataWriter
import io.micronaut.inject.annotation.AnnotationTransformer
import io.micronaut.inject.ast.ClassElement
import io.micronaut.inject.ast.GenericPlaceholderElement
import io.micronaut.inject.ast.WildcardElement
import io.micronaut.inject.provider.BeanProviderDefinition
import io.micronaut.inject.provider.JakartaProviderBeanDefinition
import io.micronaut.inject.visitor.TypeElementVisitor
import io.micronaut.inject.writer.BeanConfigurationWriter
import io.micronaut.inject.writer.BeanDefinitionVisitor
import io.micronaut.inject.writer.BeanDefinitionWriter
import org.intellij.lang.annotations.Language
import spock.lang.Specification

import javax.lang.model.element.Element
import javax.lang.model.element.ExecutableElement
import javax.lang.model.element.TypeElement
import javax.lang.model.element.VariableElement
import javax.tools.JavaFileObject
import java.lang.annotation.Annotation
import java.util.stream.Collectors
import java.util.stream.StreamSupport
/**
 * Base class to extend from to allow compilation of Java sources
 * at runtime to allow testing of compile time behavior.
 *
 * @author Graeme Rocher
 * @since 1.0
 */
abstract class AbstractTypeElementSpec extends Specification {

    /**
     * Builds a class element for the given source code.
     * @param cls The source
     * @return The class element
     * @deprecated Use closure equivalent and supply assertions
     */
    @Deprecated
    ClassElement buildClassElement(@Language("java") String cls) {
        TypeElementInfo typeElementInfo = buildTypeElementInfo(cls)
        TypeElement typeElement = typeElementInfo.typeElement
        def lastTask = typeElementInfo.javaParser.lastTask.get()
        def elements = lastTask.elements
        def types = lastTask.types
        def processingEnv = typeElementInfo.javaParser.processingEnv
        def messager = processingEnv.messager
        ModelUtils modelUtils = new ModelUtils(elements, types) {}

        JavaVisitorContext visitorContext = new JavaVisitorContext(
                processingEnv,
                messager,
                elements,
                types,
                modelUtils,
                processingEnv.filer,
                new MutableConvertibleValuesMap<Object>(),
                TypeElementVisitor.VisitorKind.ISOLATING
        )

        return new JavaElementFactory(visitorContext).newClassElement(typeElement, visitorContext.getElementAnnotationMetadataFactory())
    }

    <T> T buildClassElement(@Language("java") String packageInfo, @Language("java") String cls, Closure<T> closure) {
        return buildClassElement(new JavaFiles().add("Test", cls).add("package-info", packageInfo), closure)
    }

    <T> T buildClassElement(@Language("java") String cls, Closure<T> closure) {
        return buildClassElement(new JavaFiles().add("", cls), closure)
    }
    protected <T> T buildClassElement(JavaFiles files, Closure<T> closure) {
        buildTypeElementInfo(files) { TypeElementInfo typeElementInfo ->
            TypeElement typeElement = typeElementInfo.typeElement
            def lastTask = typeElementInfo.javaParser.lastTask.get()
            def elements = lastTask.elements
            def types = lastTask.types
            def processingEnv = typeElementInfo.javaParser.processingEnv
            def messager = processingEnv.messager
            ModelUtils modelUtils = new ModelUtils(elements, types) {}

            JavaVisitorContext visitorContext = new JavaVisitorContext(
                    processingEnv,
                    messager,
                    elements,
                    types,
                    modelUtils,
                    processingEnv.filer,
                    new MutableConvertibleValuesMap<Object>(),
                    TypeElementVisitor.VisitorKind.ISOLATING
            )

            def classElement = new JavaElementFactory(visitorContext).newClassElement(typeElement, visitorContext.getElementAnnotationMetadataFactory())
            return closure.call(classElement)
        }
    }

    /**
     * @param cls The class string
     * @return The annotation metadata for the class
     */
    @CompileStatic
    AnnotationMetadata buildTypeAnnotationMetadata(@Language("java") String cls) {
        AbstractAnnotationMetadataBuilder.clearMutated()
        Element element = buildTypeElement(cls)
        JavaAnnotationMetadataBuilder builder = newJavaAnnotationBuilder()
        AnnotationMetadata metadata = element != null ? builder.lookupOrBuildForType(element) : null
        AbstractAnnotationMetadataBuilder.copyToRuntime()
        return metadata
    }

    AnnotationMetadata buildMethodArgumentAnnotationMetadata(@Language("java") String cls, String methodName, String argumentName) {
        AbstractAnnotationMetadataBuilder.clearMutated()
        TypeElement element = buildTypeElement(cls)
        ExecutableElement method = (ExecutableElement)element.getEnclosedElements().find() { it.simpleName.toString() == methodName }
        VariableElement argument = method.parameters.find() { it.simpleName.toString() == argumentName }
        JavaAnnotationMetadataBuilder builder = newJavaAnnotationBuilder()
        AnnotationMetadata metadata = argument != null ? builder.lookupOrBuildForMethod(element, argument) : null
        AbstractAnnotationMetadataBuilder.copyToRuntime()
        return metadata
    }

    /**
     * Build and return a {@link BeanIntrospection} for the given class name and class data.
     *
     * @return the introspection if it is correct
     **/
    protected BeanIntrospection buildBeanIntrospection(String className, @Language("java") String cls) {
        def simpleName = NameUtils.getSimpleName(className)
        def beanDefName = (simpleName.startsWith('$') ? '' : '$') + simpleName + '$Introspection'
        def packageName = NameUtils.getPackageName(className)
        String beanFullName = "${packageName}.${beanDefName}"

        ClassLoader classLoader = buildClassLoader(className, cls)
        return (BeanIntrospection)classLoader.loadClass(beanFullName).newInstance()
    }

    /**
     * Build and return a {@link GraalReflectionConfigurer} for the given class name and class data.
     *
     * @return the GraalReflectionConfigurer if it is correct
     **/
    protected GraalReflectionConfigurer buildReflectionConfigurer(String className, @Language("java") String cls) {
        String beanDefName = (className.startsWith('$') ? '' : '$') + NameUtils.getSimpleName(className) + GraalReflectionConfigurer.CLASS_SUFFIX
        String packageName = NameUtils.getPackageName(className)
        String beanFullName = "${packageName}.${beanDefName}"

        ClassLoader classLoader = buildClassLoader(className, cls)
        return (GraalReflectionConfigurer)classLoader.loadClass(beanFullName).newInstance()
    }

    /**
     * @param annotationExpression the annotation expression
     * @param packages the packages to import
     * @return The metadata
     */
    @CompileStatic
    AnnotationMetadata buildAnnotationMetadata(String annotationExpression, String... packages) {

        List<String> packageList = ["io.micronaut.core.annotation",
                                    "io.micronaut.inject.annotation"]
        packageList.addAll(Arrays.asList(packages))
        packageList = packageList.unique()
        return buildTypeAnnotationMetadata("""
${packageList.collect() { "import ${it}.*;" }.join(System.getProperty('line.separator'))}

${annotationExpression}
class Test {

}
""")
    }

    /**
     * Reads a generated file
     * @param filePath The file path
     * @param className The class name
     * @param code The code
     * @return The reader
     * @throws IOException
     */
    public @Nullable Reader readGenerated(@NonNull String filePath, String className, @Language("java") String code) throws IOException {
        try (def parser = newJavaParser()) {
            return parser.readGenerated(filePath, className, code)
        }
    }

    /**
     * Gets a bean from the context for the given class name
     * @param context The context
     * @param className The class name
     * @return The bean instance
     */
    Object getBean(ApplicationContext context, String className, Qualifier qualifier = null) {
        context.getBean(context.classLoader.loadClass(className), qualifier)
    }


    /**
     * Gets a bean definition from the context for the given class name
     * @param context The context
     * @param className The class name
     * @return The bean instance
     */
    BeanDefinition<?> getBeanDefinition(ApplicationContext context, String className, Qualifier qualifier = null) {
        context.getBeanDefinition(context.classLoader.loadClass(className), qualifier)
    }

    /**
     * Builds a {@link ApplicationContext} containing only the classes produced by the given source.
     *
     * @param source The source code
     * @return The context. Should be shutdown after use
     */
    ApplicationContext buildContext(@Language("java") String source) {
        return buildContext("test.Source" + System.currentTimeMillis(), source)
    }

    /**
     * Builds a {@link ApplicationContext} containing only the classes produced by the given class.
     *
     * @param className The class name
     * @param cls The class data
     * @return The context. Should be shutdown after use
     */
    ApplicationContext buildContext(String className, @Language("java") String cls, boolean includeAllBeans = false, Map properties = [:]) {
        return buildContext(null, className, cls, includeAllBeans, properties)
    }

    /**
     * Builds a {@link ApplicationContext} containing only the classes produced by the given class.
     *
     * @param className The class name
     * @param cls The class data
     * @return The context. Should be shutdown after use
     */
    ApplicationContext buildContext(@Nullable @Language("java") String packageJava, String className, @Language("java") String cls, boolean includeAllBeans = false, Map properties = [:]) {
        JavaFiles files = new JavaFiles()
        files.add(className, cls)
        if (packageJava != null) {
            files.add("package-info", packageJava)
        }
        return buildContext(files, includeAllBeans, properties)
    }

    /**
     * Builds a {@link ApplicationContext} containing only the classes produced by the given class.
     *
     * @param className The class name
     * @param cls The class data
     * @return The context. Should be shutdown after use
     */
    ApplicationContext buildContext(JavaFiles files, boolean includeAllBeans = false, Map properties = [:]) {
        try (def parser = newJavaParser()) {
            def javaFiles = parser.generate(
                    files.files.stream().map { JavaFileObjects.forSourceString(it.key, it.value) }.toArray(JavaFileObject[]::new)
            )
            ClassLoader classLoader = new JavaFileObjectClassLoader(javaFiles)

            def builder = ApplicationContext.builder()
            builder.classLoader(classLoader)
            builder.environments("test")
            builder.properties(properties)
            configureContext(builder)
            builder.beanDefinitionsProvider {
                def references = StreamSupport.stream(javaFiles.spliterator(), false)
                        .filter({ JavaFileObject jfo ->
                            jfo.kind == JavaFileObject.Kind.CLASS && (jfo.name.endsWith(BeanDefinitionWriter.CLASS_SUFFIX + '$Reference' + ".class") ||  jfo.name.endsWith(BeanDefinitionWriter.CLASS_SUFFIX + ".class"))
                        })
                        .map({ JavaFileObject jfo ->
                            def name = jfo.toUri().toString().substring("mem:///CLASS_OUTPUT/".length())
                            name = name.replace('/', '.') - '.class'
                            return (BeanDefinitionReference) classLoader.loadClass(name).newInstance()
                        })
                        .collect(Collectors.toList())

                return references + (includeAllBeans ? new DefaultBeanDefinitionsProvider().provide(it) : getBuiltInBeanReferences())
            }
            return builder.build().start()
        }
    }

    /**
     * @return Obtains the built in bean references required for the context to function correctly
     */
    List<BeanDefinitionReference<?>> getBuiltInBeanReferences() {
        return [
                new InterceptorRegistryBean(),
                new BeanProviderDefinition(),
                new JakartaProviderBeanDefinition(),
                new ApplicationEventPublisherFactory<>()
        ]
    }

    /**
     * Create and return a new Java parser.
     * @return The java parser to use
     */
    protected JavaParser newJavaParser() {
        def visitors = getLocalTypeElementVisitors()
        if (visitors) {
            return new JavaParser() {
                @Override
                protected TypeElementVisitorProcessor getTypeElementVisitorProcessor() {
                    return new TypeElementVisitorProcessor() {
                        @NonNull
                        @Override
                        protected Collection<TypeElementVisitor> findTypeElementVisitors() {
                            return visitors
                        }
                    }
                }

                @Override
                protected AggregatingTypeElementVisitorProcessor getAggregatingTypeElementVisitorProcessor() {
                    return new AggregatingTypeElementVisitorProcessor() {
                        @NonNull
                        @Override
                        protected Collection<TypeElementVisitor> findTypeElementVisitors() {
                            return visitors
                        }
                    }
                }

            }
        } else {
            return new JavaParser()
        }
    }

    /**
     * @param cls   The class string
     * @param methodName The method name
     * @return The annotation metadata for the method
     */
    @CompileStatic
    AnnotationMetadata buildMethodAnnotationMetadata(@Language("java") String cls, String methodName) {
        TypeElement element = buildTypeElement(cls)
        Element method = element.getEnclosedElements().find() { it.simpleName.toString() == methodName }
        JavaAnnotationMetadataBuilder builder = newJavaAnnotationBuilder()
        AnnotationMetadata metadata = method != null ? builder.lookupOrBuildForMethod(element, method) : null
        return metadata
    }

    protected TypeElement buildTypeElement(@Language('java') String cls) {
        List<Element> elements = []

        try (def parser = newJavaParser()) {
            parser.parseLines("",
                    cls
            ).each { elements.add(it) }
        }

        def element = elements ? elements[0] : null
        return (TypeElement) element
    }

    protected TypeElementInfo buildTypeElementInfo(@Language("java") String cls) {
        List<Element> elements = []

        try (def parser = newJavaParser()) {
            parser.parseLines("",
                    cls
            ).each { elements.add(it) }
            def element = elements ? elements[0] : null
            return new TypeElementInfo(
                    typeElement: element,
                    javaParser: parser
            )
        }

    }

    protected <T> T buildTypeElementInfo(JavaFiles files, Closure<T> callable) {
        try (def parser = newJavaParser()) {
            JavaFileObject[] sources = files.files.stream()
                    .map { e -> JavaFileObjects.forSourceLines(e.key, e.value) }
                    .toArray(JavaFileObject[]::new)
            Iterator<? extends Element> elements = parser.parse(sources).iterator()

            TypeElement element = null
            for (Element e : elements) {
                if (e instanceof TypeElement) {
                    element = e
                    break
                }
            }

            return callable.call( new TypeElementInfo(
                    typeElement: element,
                    javaParser: parser
            ))
        }

    }

    protected String buildAndReadResourceAsString(String resourceName, @Language("java") String cls) {
        ClassLoader classLoader = buildClassLoader("test.Test", cls)
        return IOUtils.readText(new BufferedReader(new InputStreamReader(classLoader.getResources(resourceName).toList().last().openStream())))
    }

    protected BeanDefinition buildBeanDefinition(String className, @Language("java") String cls) {
        def classSimpleName = NameUtils.getSimpleName(className)
        def beanDefName = (classSimpleName.startsWith('$') ? '' : '$') + classSimpleName + BeanDefinitionWriter.CLASS_SUFFIX
        def packageName = NameUtils.getPackageName(className)
        String beanFullName = "${packageName}.${beanDefName}"

        ClassLoader classLoader = buildClassLoader(className, cls)
        try {
            return (BeanDefinition)classLoader.loadClass(beanFullName).newInstance()
        } catch (ClassNotFoundException e) {
            return null
        }
    }

    protected BeanDefinition buildBeanDefinition(String packageName, String className, @Language("java") String cls) {
        def beanDefName= (className.startsWith('$') ? '' : '$') + className + BeanDefinitionWriter.CLASS_SUFFIX
        String beanFullName = "${packageName}.${beanDefName}"

        ClassLoader classLoader = buildClassLoader(className, cls)
        try {
            return (BeanDefinition)classLoader.loadClass(beanFullName).newInstance()
        } catch (ClassNotFoundException e) {
            return null
        }
    }

    /**
     * Builds the bean definition for an AOP proxy bean.
     * @param className The class name
     * @param cls The class source
     * @return The bean definition
     */
    protected BeanDefinition buildInterceptedBeanDefinition(String className, @Language("java") String cls) {
        def classSimpleName = NameUtils.getSimpleName(className)
        def beanDefName = (classSimpleName.startsWith('$') ? '' : '$') + classSimpleName + BeanDefinitionWriter.CLASS_SUFFIX + BeanDefinitionVisitor.PROXY_SUFFIX + BeanDefinitionWriter.CLASS_SUFFIX
        def packageName = NameUtils.getPackageName(className)
        String beanFullName = "${packageName}.${beanDefName}"

        ClassLoader classLoader = buildClassLoader(className, cls)
        return (BeanDefinition)classLoader.loadClass(beanFullName).newInstance()
    }

    /**
     * Builds the bean definition for an AOP proxy bean.
     * @param className The class name
     * @param cls The class source
     * @return The bean definition
     */
    protected BeanDefinition buildSimpleInterceptedBeanDefinition(String className, @Language("java") String cls) {
        def classSimpleName = NameUtils.getSimpleName(className)
        def beanDefName = (classSimpleName.startsWith('$') ? '' : '$') + classSimpleName + BeanDefinitionVisitor.PROXY_SUFFIX + BeanDefinitionWriter.CLASS_SUFFIX
        def packageName = NameUtils.getPackageName(className)
        String beanFullName = "${packageName}.${beanDefName}"

        ClassLoader classLoader = buildClassLoader(className, cls)
        return (BeanDefinition)classLoader.loadClass(beanFullName).newInstance()
    }

    /**
     * Retrieve additional annotation mappers to apply
     * @param annotationName The annotation name
     * @return The mappers for the annotation
     */
    protected List<AnnotationMapper<? extends Annotation>> getLocalAnnotationMappers(@NonNull String annotationName) {
        return Collections.emptyList()
    }

    /**
     * Retrieve additional annotation transformers  to apply
     * @param annotationName The annotation name
     * @return The transformers for the annotation
     */
    protected List<AnnotationTransformer<? extends Annotation>> getLocalAnnotationTransformers(@NonNull String annotationName) {
        return Collections.emptyList()
    }

    /**
     * Retrieve additional type element visitors for this test.
     * @return the visitors
     */
    protected Collection<TypeElementVisitor> getLocalTypeElementVisitors() {
        return Collections.emptyList()
    }

    /**
     * Builds the bean definition reference for an AOP proxy bean.
     * @param className The class name
     * @param cls The class source
     * @return The bean definition
     */
    protected BeanDefinitionReference buildInterceptedBeanDefinitionReference(String className, @Language("java") String cls) {
        def classSimpleName = NameUtils.getSimpleName(className)
        def beanDefName = (classSimpleName.startsWith('$') ? '' : '$') + classSimpleName + BeanDefinitionWriter.CLASS_SUFFIX + BeanDefinitionVisitor.PROXY_SUFFIX + BeanDefinitionWriter.CLASS_SUFFIX
        def packageName = NameUtils.getPackageName(className)
        String beanFullName = "${packageName}.${beanDefName}"

        ClassLoader classLoader = buildClassLoader(className, cls)
        return (BeanDefinitionReference)classLoader.loadClass(beanFullName).newInstance()
    }

    protected BeanDefinitionReference buildBeanDefinitionReference(String className, @Language("java") String cls) {
        def classSimpleName = NameUtils.getSimpleName(className)
        def beanDefName= (classSimpleName.startsWith('$') ? '' : '$') + classSimpleName + BeanDefinitionWriter.CLASS_SUFFIX
        def packageName = NameUtils.getPackageName(className)
        String beanFullName = "${packageName}.${beanDefName}"

        ClassLoader classLoader = buildClassLoader(className, cls)
        return (BeanDefinitionReference)classLoader.loadClass(beanFullName).newInstance()
    }

    protected BeanConfiguration buildBeanConfiguration(String packageName, @Language("java") String cls) {
        ClassLoader classLoader = buildClassLoader("${packageName}.package-info", cls)
        return (BeanConfiguration)classLoader.loadClass(packageName + '.' + BeanConfigurationWriter.CLASS_SUFFIX).newInstance()
    }

    @CompileStatic
    protected ClassLoader buildClassLoader(String className, @Language("java") String cls) {
        AbstractAnnotationMetadataBuilder.clearMutated()
        try (def parser = newJavaParser()) {
            Iterable<? extends JavaFileObject> files = parser.generate(className, cls)
            return new JavaFileObjectClassLoader(files)
        }
    }

    @CompileStatic
    protected AnnotationMetadata writeAndLoadMetadata(String className, AnnotationMetadata toWrite) {
        byte[] bytecode = AnnotationMetadataWriter.write(className, toWrite)
        className = className + AnnotationMetadata.CLASS_NAME_SUFFIX
        ClassLoader classLoader = new DefiningClassLoader(className, bytecode)
        return ((AnnotationMetadataProvider) classLoader.loadClass(className).newInstance()).getAnnotationMetadata()
    }

    protected JavaAnnotationMetadataBuilder newJavaAnnotationBuilder() {
        try (JavaParser parser = newJavaParser()) {
            JavacTask javacTask = parser.getJavacTask()
            def elements = javacTask.elements
            def types = javacTask.types
            def processingEnv = parser.processingEnv
            def messager = processingEnv.messager
            ModelUtils modelUtils = new ModelUtils(elements, types) {}
            JavaVisitorContext visitorContext = new JavaVisitorContext(
                    processingEnv,
                    messager,
                    elements,
                    types,
                    modelUtils,
                    parser.filer,
                    new MutableConvertibleValuesMap<>(),
                    TypeElementVisitor.VisitorKind.ISOLATING
            );
            JavaNativeElementsHelper helper = new JavaNativeElementsHelper(elements, modelUtils.getTypeUtils())
            JavaAnnotationMetadataBuilder builder = new JavaAnnotationMetadataBuilder(elements, messager, modelUtils, helper, visitorContext) {
                @Override
                protected List<AnnotationMapper<? extends Annotation>> getAnnotationMappers(@NonNull String annotationName) {
                    def loadedMappers = super.getAnnotationMappers(annotationName)
                    def localMappers = getLocalAnnotationMappers(annotationName)
                    if (localMappers) {
                        def newList = []
                        if (loadedMappers) {
                            newList.addAll(loadedMappers)
                        }
                        newList.addAll(localMappers)
                        return newList
                    } else {
                        if (localMappers) {
                            return loadedMappers
                        } else {
                            return Collections.emptyList()
                        }
                    }
                }

                @Override
                protected List<AnnotationTransformer<Annotation>> getAnnotationTransformers(@NonNull String annotationName) {
                    def loadedTransformers = super.getAnnotationTransformers(annotationName)
                    def localTransformers = getLocalAnnotationTransformers(annotationName)
                    if (localTransformers) {
                        def newList = []
                        if (loadedTransformers) {
                            newList.addAll(loadedTransformers)
                        }
                        newList.addAll(localTransformers)
                        return newList
                    } else {
                        return loadedTransformers
                    }
                }
            }
            return builder
        }
    }

    /**
     * Allows configuring the context
     * @param contextBuilder The context builder
     */
    protected void configureContext(ApplicationContextBuilder contextBuilder) {
    }

    /**
     * Create a rough source signature of the given ClassElement, using {@link ClassElement#getBoundGenericTypes()}.
     * Can be used to test that {@link ClassElement#getBoundGenericTypes()} returns the right types in the right
     * context.
     *
     * @param classElement The class element to reconstruct
     * @param typeVarsAsDeclarations Whether type variables should be represented as declarations
     * @return a String representing the type signature.
     */
    @Experimental
    protected static String reconstructTypeSignature(ClassElement classElement, boolean typeVarsAsDeclarations = false) {
        if (classElement.isArray()) {
            return reconstructTypeSignature(classElement.fromArray()) + "[]"
        } else if (classElement.isGenericPlaceholder()) {
            def genericPlaceholderElement = (GenericPlaceholderElement) classElement
            def name = genericPlaceholderElement.variableName
            if (typeVarsAsDeclarations) {
                def bounds = genericPlaceholderElement.bounds
                if (reconstructTypeSignature(bounds[0]) != 'Object') {
                    name += bounds.stream().map(AbstractTypeElementSpec::reconstructTypeSignature).collect(Collectors.joining(" & ", " extends ", ""))
                }
            } else if (genericPlaceholderElement.resolved) {
                return reconstructTypeSignature(genericPlaceholderElement.resolved.get())
            }
            return name
        } else if (classElement.isWildcard()) {
            def we = (WildcardElement) classElement
            if (!we.lowerBounds.isEmpty()) {
                return we.lowerBounds.stream().map(AbstractTypeElementSpec::reconstructTypeSignature).collect(Collectors.joining(" | ", "? super ", ""))
            } else if (we.upperBounds.size() == 1 && reconstructTypeSignature(we.upperBounds.get(0)) == "Object") {
                return "?"
            } else {
                return we.upperBounds.stream().map(AbstractTypeElementSpec::reconstructTypeSignature).collect(Collectors.joining(" & ", "? extends ", ""))
            }
        } else {
            def typeArguments = classElement.getTypeArguments().values()
            if (typeArguments.isEmpty()) {
                return classElement.getSimpleName()
            } else if (typeArguments.stream().allMatch { it.isRawType() }) {
                return classElement.getSimpleName()
            } else {
                return classElement.getSimpleName() + typeArguments.stream().map(AbstractTypeElementSpec::reconstructTypeSignature).collect(Collectors.joining(", ", "<", ">"))
            }
        }
    }

    static class TypeElementInfo {
        TypeElement typeElement
        JavaParser javaParser
    }

    @CompileStatic
    static class JavaFiles {

        private List<Map.Entry<String, String>> files = new ArrayList<>()

        JavaFiles add(String filename, @Language("java") String code) {
            files.add(Map.entry(filename, code))
            return this
        }

        List<Map.Entry<String, String>> getFiles() {
            return files
        }

    }


    // Workaround for Groovy 5 compiler bug
    // java.lang.IllegalAccessException: class org.codehaus.groovy.reflection.CachedMethod cannot access a member of class java.lang.ClassLoader (in module java.base) with modifiers "protected final"
    //	at io.micronaut.ast.transform.test.AbstractBeanDefinitionSpec$2.findClass(AbstractBeanDefinitionSpec.groovy:229)
    //	at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:593)
    //	at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:526)
    //	at io.micronaut.ast.transform.test.AbstractBeanDefinitionSpec.writeAndLoadMetadata(AbstractBeanDefinitionSpec.groovy:235)
    //	at io.micronaut.ast.groovy.annotation.router.GroovyAnnotationMetadataBuilderSpec.test enum value action annotation metadata(GroovyAnnotationMetadataBuilderSpec.groovy:51)
    @CompileStatic
    private static class DefiningClassLoader extends ClassLoader {
        private final String className
        private final byte[] bytecode

        DefiningClassLoader(String className, byte[] bytecode) {
            this.className = className
            this.bytecode = bytecode
        }

        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            if (name == className) {
                return super.defineClass(name, bytecode, 0, bytecode.length)
            }
            return super.findClass(name)
        }
    }
}
